package com.pubgnode.config;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pubgnode.constant.AuthConstants;
import com.pubgnode.constant.BusinessEnum;
import com.pubgnode.constant.HttpConstants;
import com.pubgnode.impl.UserDetailsServiceImpl;
import com.pubgnode.model.LoginResult;
import com.pubgnode.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.UUID;

/**
 * Security安全框架配置类
 */
@Configuration
@Slf4j
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 设置Security安全框架走自己的认证流程
     * @param auth
     * @throws Exception
     */
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * auth-server具体的流程：
     * 1.在对gateway-server中对路径进行放行之后，路由到auth-server中的认证流程，即我们自己创建的认证流程
     *   我们自己的认证流程写在UserDetailServiceImpl中
     * 2.在认证流程中，我们首先从请求头中获取登陆类型，根据登陆类型进行调用不同的登陆策略
     *   其中登陆策略通过登陆策略对象实现
     * 3.登陆策略对象在LoginStrategyFactory中，工厂类的作用是用来构建具体策略的实例，将每个登陆策略放进map集合
     *   ，根据用户登陆类型获取具体的登陆策略
     * 4.而具体的策略写在strategy包中impl包下，本项目中分为SysUserLoginStrategy--商城后台管理系统登陆具体实现策略
     *   以及MemberLoginStrategy--商城用户购物系统登陆具体实现策略
     * 5.在具体策略中，拿SysUserLoginStrategy举例来说，我们首先根据用户名查询用户是否存在，用户不存在，则登陆失败，存在，
     *   则Security框架会调用match来验证用户密码，如果密码错误也会登陆失败
     * 6.用户名及密码均正确，则会查询用户的操作权限，在查询权限之后，将用户对象放到Security上下文之中，由于Security框架
     *   只认识UserDetails对象，因此封装一个SecurityUser对象（UserDetails类的子类）进行对用户信息的封装并
     *   返回存放用户信息的对象到上下文之中
     * 7.若登陆成功，使用登陆成功处理器进行处理，从上下文对象（authentication）之中取出用户信息对象并转化为json格式的
     *   字符串，并作为value放到redis当中（key为token--使用uuid作为token）
     * 8.封装一个登陆统一结果对象用来对登陆结果进行统一的反馈，其中该对象封装了token以及token的有效时长。
     */



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //首先在UserDetailServiceImpl中重写认证流程
        //然后在配置类中创建实体类对象
        //将重写的认证流程添加到配置类重写的方法中，实现安全框架走自己的认证流程
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭跨站请求伪造
        http.cors().disable();
        //关闭跨域请求
        http.csrf().disable();
        //关闭session使用策略
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //配置登录信息
        http.formLogin()
                .loginProcessingUrl(AuthConstants.LOGIN_URL) // 设置登录url
                .successHandler(authenticationSuccessHandler()) // 设置登录成功处理器
                .failureHandler(authenticationFailureHandler()); // 设置登录失败处理器
        //配置登出信息
        http.logout()
                .logoutUrl(AuthConstants.LOGOUT_URL) // 设置登出url
                .logoutSuccessHandler(logoutSuccessHandler()); // 设置登出成功处理器

        //要求所有请求都需要进行身份的验证
        http.authorizeHttpRequests().anyRequest().authenticated();
    }


    /**
     * 登陆成功处理器
     * @return
     */
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler(){
        return (request, response, authentication) -> {
            //设置响应头信息
            response.setContentType(HttpConstants.APPLICATION_JSON);
            response.setCharacterEncoding(HttpConstants.UTF_8);

            //使用uuid当作token
            //token相当于一个key，其对应的值为对应的用户信息
            String token = UUID.randomUUID().toString();
            //从Security框架中获取认证用户对象（SecurityUser）并转化为json格式的字符串
            String userJsonStr = JSONObject.toJSONString(authentication.getPrincipal());
            //将token当作key，认证用户对象的json格式的字符串当作当前value存放到redis中
            stringRedisTemplate.opsForValue().set(AuthConstants.LOGIN_TOKEN_PREFIX+token,userJsonStr,
                    Duration.ofSeconds(AuthConstants.TOKEN_TIME));

            //封装一个登陆统一结果对象（LoginResult）
            LoginResult loginResult = new LoginResult(token, AuthConstants.TOKEN_TIME);

            //创建一个响应结果对象
            Result<Object> result = Result.success(loginResult);

            //返回结果
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);
            PrintWriter writer = response.getWriter();
            writer.write(s);
            writer.flush();
            writer.close();

        };
    }


    /**
     * 登陆失败处理器
     * @return
     */
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler(){
        return (request, response, exception) -> {
            //设置响应头信息
            response.setContentType(HttpConstants.APPLICATION_JSON);
            response.setCharacterEncoding(HttpConstants.UTF_8);

            //创建统一响应结果对象
            Result<Object> result = new Result<>();
            result.setCode(BusinessEnum.OPERATION_FAIL.getCode());
            if (exception instanceof BadCredentialsException) {
                result.setMsg("用户名或密码有误");
            } else if (exception instanceof UsernameNotFoundException) {
                result.setMsg("用户不存在");
            } else if (exception instanceof AccountExpiredException) {
                result.setMsg("账号异常,请联系管理员");
            } else if (exception instanceof AccountStatusException) {
                result.setMsg("账号异常,请联系管理员");
            } else if (exception instanceof InternalAuthenticationServiceException) {
                result.setMsg(exception.getMessage());
            } else {
                result.setMsg(BusinessEnum.OPERATION_FAIL.getDesc());
            }

            //返回结果
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);
            PrintWriter writer = response.getWriter();
            writer.write(s);
            writer.flush();
            writer.close();

        };
    }

    /**
     * 登出成功处理器
     * @return
     */
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler(){
        return (request, response, authentication) -> {
            //设置响应头信息
            response.setContentType(HttpConstants.APPLICATION_JSON);
            response.setCharacterEncoding(HttpConstants.UTF_8);

            //从请求头中获取token
            String authorization = request.getHeader(AuthConstants.AUTHORIZATION);
            String token = authorization.replaceFirst(AuthConstants.BEARER,"");
            //将当前token从redis中剔除
            stringRedisTemplate.delete(AuthConstants.LOGIN_TOKEN_PREFIX+token);

            //创建统一响应结果对象
            Result<String> result = new Result<>();
            //返回结果
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);
            PrintWriter writer = response.getWriter();
            writer.write(s);
            writer.flush();
            writer.close();

        };
    }

    /**
     * 密码加密器
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
